<?php
class Pageley_Mapper_GatewayTests extends Pageley_Test_Case {

    private $mockFactory;
	private $type;
    private $type2;
    private $relationType;
    private $categoryType;
    private $keywordType;
	private $gateway;
	private $transaction;
	private $unitOfWork;

	public function setUp() {
        $string = new Pageley_Domain_Attribute_Basic("string");
		$number = new Pageley_Domain_Attribute_Basic("number");
		$namespace = new Pageley_Domain_BasicNamespace("urn:commerce.pageley.org:1.0", "commerce", "commerce");
		$this->categoryType = new Pageley_Domain_BasicType(
			$namespace,
			"Category"
		);
        $this->keywordType = new Pageley_Domain_BasicType(
			$namespace,
			"Keyword"
		);
        $category = new Pageley_Domain_Attribute_Reference("category", $this->categoryType);
        $this->categoryType->setAttributes(
			new Pageley_Util_ArrayMap(array(
				"name" => $string,
                "parentCategory" => $category
			)));
        $keyword = new Pageley_Domain_Attribute_Reference("keyword", $this->keywordType);
		$this->type = new Pageley_Domain_BasicType(
			$namespace,
			"Article",
			new Pageley_Util_ArrayMap(array(
				"name" => $string,
				"price" => $number,
                "category" => $category,
                "keywords" => new Pageley_Domain_Attribute_OneToManyDecorator($keyword)
			))
		);
        $this->type2 = new Pageley_Domain_BasicType(
            $namespace,
            "Article",
            new Pageley_Util_ArrayMap(array(
                "name" => $string,
                "price" => $number,
                "category" => $category,
                "keywords" => new Pageley_Domain_Attribute_ManyToManyDecorator($keyword)
            ))
        );
        $referencingTypeAttribute = new Pageley_Domain_Attribute_Reference("article", $this->type2);
        $referencedTypeAttribute = new Pageley_Domain_Attribute_Reference("keyword", $this->keywordType);
        $this->relationType = new Pageley_Domain_BasicType(
                $namespace,
                "ArticleKeyword",
                new Pageley_Util_ArrayMap(array(
                    "article" => new Pageley_Domain_Attribute_RequiredDecorator($referencingTypeAttribute),
                    "keyword" => new Pageley_Domain_Attribute_RequiredDecorator($referencedTypeAttribute)
                )));
        
		$article = new Pageley_Domain_Attribute_Reference("article", $this->type);
		$this->keywordType->setAttributes(
			new Pageley_Util_ArrayMap(array(
				"name" => $string,
				"article" => $article
			)));
        $this->mockFactory = new Pageley_Mock_Factory();
		$this->gateway = $this->mockFactory->createMock(new Pageley_Util_ArrayMap("Pageley_Gateway"));
        $this->transaction = $this->mockFactory->createMock(new Pageley_Util_ArrayMap("Pageley_Gateway_Database_Transaction"));
		$this->unitOfWork = new Pageley_Mapper_Gateway($this->gateway);
	}
    
    public function tearDown() {
        $this->mockFactory->finish();
    }
    
    public function setId($parameters) {
        $parameters->get(0)->setId(1);
    }

	public function testQuery() {
        $query = new Pageley_Query_Basic($this->type);
                
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->count($query)->andReturn(3);
        $this->gateway->query($query)->andReturn(new Pageley_Util_ArrayMap($this->createEntity1(), $this->createEntity2(), $this->createEntity3()));
        $this->gateway->count($query)->andReturn(3);
        $this->gateway->query($query)->andReturn(new Pageley_Util_ArrayMap($this->createEntity1(), $this->createEntity2(), $this->createEntity3()));
    
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
		$entities1 = $this->unitOfWork->query($query);
        $this->assertEquals(3, $entities1->size());
        $this->assertNotNull($entities1->get(0));
        $this->assertNotNull($entities1->get(1));
        $this->assertNotNull($entities1->get(2));
		$entities2 = $this->unitOfWork->query($query);
        $this->assertEquals(3, $entities2->size());
		$this->assertSame($entities1->get(0), $entities2->get(0));
		$this->assertSame($entities1->get(1), $entities2->get(1));
		$this->assertSame($entities1->get(2), $entities2->get(2));
	}

	public function testFind() {
		// Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->find($this->type, 1)->andReturn($this->createEntity1());
        $this->gateway->find($this->type, 1)->andReturn($this->createEntity1());

        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity1 = $this->unitOfWork->find($this->type, 1);
		$entity2 = $this->unitOfWork->find($this->type, 1);
        $this->assertNotNull($entity1);
		$this->assertSame($entity1, $entity2);
	}

	public function testCommitAfterRead() {
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->find($this->type, 2)->andReturn($this->createEntity2());
        $this->transaction->commit();
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
		$entity = $this->unitOfWork->find($this->type, 2);
		$this->unitOfWork->commit();
	}

	public function testCommitAfterRegister() {
        $entity = $this->createEntity2();
        $entity->setId(null);

		// Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->insert($entity)->andExecute($this, "setId");
        $this->transaction->commit();
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity = $this->createEntity2();
        $entity->setId(null);
        $this->unitOfWork->register($entity);
		$this->unitOfWork->commit();
	}

	public function testCommitAfterUpdate() {
        $entity = $this->createEntity1();
        $entity->set("name", "Donkey spray");
        
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->find($this->type, 2)->andReturn($entity);
        $this->gateway->update($entity);
        $this->transaction->commit();
    
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
		$entity = $this->unitOfWork->find($this->type, 2);
		$entity->set("name", "Donkey spray");
		$this->unitOfWork->commit();
	}

	public function testCommitAfterDelete() {
		$entity = $this->createEntity2();
        $query = new Pageley_Query_Basic($this->keywordType, new Pageley_Query_Condition_Equals("article", $entity));
        
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->find($this->type, 2)->andReturn($entity);
        $this->gateway->count($query)->andReturn(0);
        $this->gateway->delete($entity);
        $this->transaction->commit();
    
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity = $this->unitOfWork->find($this->type, 2);
		$this->unitOfWork->unregister($entity);
		$this->unitOfWork->commit();
	}

	public function testRollback() {
		// Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->transaction->rollback();
    
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity = $this->createEntity1();
        $entity->setId(null);
		$this->unitOfWork->register($entity);
		$this->unitOfWork->rollback();
	}

    public function testLazyLoadReferences() {
        $returnedCategory = $this->createCategory();
    
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->find($this->type, 1)->andReturn($this->createEntity1());
        $this->gateway->find($this->categoryType, 1)->andReturn($returnedCategory);
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity = $this->unitOfWork->find($this->type, 1);
        $category = $entity->get("category");
        $this->assertSame($returnedCategory, $category);
    }

    public function testLazyLoadChildren() {
        $returnedKeywords = $this->createKeywords(3);
        $entity1 = $this->createEntity1();
    
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->find($this->type, 1)->andReturn($entity1);
        $query = new Pageley_Query_Basic($this->keywordType, new Pageley_Query_Condition_Equals("article", $entity1));
        $this->gateway->count($query)->andReturn(3);
        $this->gateway->query($query)->andReturn($returnedKeywords);
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity = $this->unitOfWork->find($this->type, 1);
        $keywords = $entity->get("keywords");
        $this->assertNotNull($keywords);
        $this->assertEquals(3, $keywords->size());
        $this->assertEquals($returnedKeywords->get(0), $returnedKeywords->get(0));
    }

    public function testLazyLoadManyToMany() {
        $relationObjects = $this->createRelationObjects();
        $entity = $this->createEntityManyToMany();
        $returnedKeywords = $this->createKeywords(3);
    
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->find($this->type, 1)->andReturn($entity);
        $query = new Pageley_Query_Basic($this->relationType, new Pageley_Query_Condition_Equals("article", $entity));
        $this->gateway->count($query)->andReturn(3);
        $this->gateway->query($query)->andReturn($relationObjects);
        
        // Replay
        $this->mockFactory->replay();
        $this->unitOfWork->start();
        $entity = $this->unitOfWork->find($this->type, 1);
        $keywords = $entity->get("keywords");
        $this->assertNotNull($keywords);
        $this->assertEquals(3, $keywords->size());
        $this->assertEquals($returnedKeywords->get(0), $returnedKeywords->get(0));
    }

	public function testCascadeUpOnInsert() {
        $entity = $this->createEntity1();
        $entity->setId(null);
        $category = $this->createCategory();
        $category->setId(null);
        $entity->set("category", $category);
        
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->insert($category)->andExecute($this, "setId");
        $category->setId(1);
        $this->gateway->insert($entity)->andExecute($this, "setId");
        $this->transaction->commit();
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity = $this->createEntity1();
        $entity->setId(null);
        $category = $this->createCategory();
        $category->setId(null);
        $entity->set("category", $category);
		$this->unitOfWork->register($entity);
		$this->unitOfWork->commit();
    }

	public function testCascadeDownOnInsert() {
        $entity = $this->createEntity2();
        $entity->setId(null);
        $keyword = $this->createKeyword(null);
        $entity->get("keywords")->add($keyword);
        
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->insert($entity)->andExecute($this, "setId");
        $entity->setId(1);
        $this->gateway->insert($keyword)->andExecute($this, "setId");
        $this->transaction->commit();
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity = $this->createEntity2();
        $keyword = $this->createKeyword(null);
        $entity->setId(null);
        $entity->get("keywords")->add($keyword);
        $this->unitOfWork->register($entity);
		$this->unitOfWork->commit();
    }

    public function testCascadeDownOnDelete() {
        $entity = $this->createEntity1();
        $keywords = $this->createKeywords(3);
        $entity->get("keywords")->addAll($keywords);
        
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->find($this->type, 1)->andReturn($entity);
        $query = new Pageley_Query_Basic($this->keywordType, new Pageley_Query_Condition_Equals("article", $entity));
        $this->gateway->count($query)->andReturn(3);
        $this->gateway->query($query)->andReturn($keywords);
        $this->gateway->delete($keywords->get(2));
        $this->gateway->delete($keywords->get(1));
        $this->gateway->delete($keywords->get(0));
        $this->gateway->delete($entity);
        $this->transaction->commit();
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $entity = $this->unitOfWork->find($this->type, 1);
        $this->unitOfWork->unregister($entity);
		$this->unitOfWork->commit();
    }

    public function testCascadeCircularTypeReference() {
        $category1 = $this->categoryType->newInstance();
        $category2 = $this->categoryType->newInstance();
        $category2->set("parentCategory", $category1);
        
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->insert($category1)->andExecute($this, "setId");
        $category1->setId(1);
        $this->gateway->insert($category2)->andExecute($this, "setId");
        $this->transaction->commit();
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        $category1 = $this->categoryType->newInstance();
        $category1->setId(null);
        $category2 = $this->categoryType->newInstance();
        $category2->setId(null);
        $category2->set("parentCategory", $category1);
        $this->unitOfWork->register($category2);
		$this->unitOfWork->commit();
    }
    
    public function testTresholdExceeded() {
        $this->unitOfWork->setQueryTreshold(2);
        $query = new Pageley_Query_Basic($this->type);
                
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->count($query)->andReturn(3);
        
        // Replay
        $this->mockFactory->replay();
		$this->unitOfWork->start();
        try {
            $this->unitOfWork->query($query);
        } catch(Pageley_Mapper_TresholdExceededException $e) {
            $this->assertEquals("Number of results (3) larger than treshold (2) for query: " . $query, $e->getMessage());
        }
    }
    
    public function testStartTwice() {
        $entity2 = $this->createEntity2();
        $entity3 = $this->createEntity3();
        $entity2->setId(null);
        $entity3->setId(null);
        
        // Prepare scenario
        $this->gateway->getTransaction(false)->andReturn($this->transaction);
        $this->gateway->insert($entity2)->andExecute($this, "setId");
        $this->gateway->insert($entity3)->andExecute($this, "setId");
        $this->transaction->commit();
        
        // Replay
        $this->mockFactory->replay();
        $this->unitOfWork->start();
        $entity = $this->createEntity2();
        $entity->setId(null);
        $this->unitOfWork->register($entity);
        $this->unitOfWork->start();
        $entity = $this->createEntity3();
        $entity->setId(null);
        $this->unitOfWork->register($entity);
        $this->unitOfWork->commit();
    }
    
    /* TODO: detect circular entity references
    public function testFailOnCircularEntityReference() {
        $category1 = $this->categoryType->newInstance();
        $category2 = $this->categoryType->newInstance();
        $category3 = $this->categoryType->newInstance();
        $category2->set("parentCategory", $category1);
        $category3->set("parentCategory", $category2);
        $category1->set("parentCategory", $category3);
        $this->unitOfWork->register($category1);
		try {
            $this->unitOfWork->commit();
            $this->fail("Expected Pageley_Mapper_Exception");
        } catch(Pageley_Mapper_Exception $e) {
            // expected exception
        }
    }
	*/

    private function createEntity1() {
		$entity1 = $this->type->newInstance();
		$entity1->setId(1);
		$entity1->set("name", "Llama spray");
		$entity1->set("price", 5.5);
        $entity1->setProperty("category", new Pageley_Domain_LazyReferenceProperty(1, $this->categoryType));
		return $entity1;
	}

	private function createEntity2() {
		$entity2 = $this->type->newInstance();
		$entity2->setId(2);
		$entity2->set("name", "Duck horn");
		$entity2->set("price", 10.0);
		return $entity2;
	}

	private function createEntity3() {
		$entity3 = $this->type->newInstance();
		$entity3->setId(3);
		$entity3->set("name", "Donkey shoes");
		$entity3->set("price", 25.0);
		return $entity3;
	}

    private function createEntityManyToMany() {
        $entity = $this->type2->newInstance();
        $entity->setId(1);
        $entity->set("name", "Llama spray");
        $entity->set("price", 5.5);
        return $entity;
    }

	private function createCategory() {
		$category = $this->categoryType->newInstance();
        $category->set("name", "Animal stuff");
		$category->setId(1);
		return $category;
	}

	private function createKeywords($number) {
		$keywords = new Pageley_Util_ArrayMap();
        for($i = 1; $i <= $number; $i++) {
            $keywords->add($this->createKeyword($i));
        }
		return $keywords;
	}

    private function createRelationObjects() {
        $relationObjects = new Pageley_Util_ArrayMap();
        for($i = 1; $i <= 3; $i++) {
            $relationObject = $this->relationType->newInstance();
            $relationObject->setId($i);
            $relationObject->setProperty("article", new Pageley_Domain_LazyReferenceProperty(1, $this->type2));
            $relationObject->set("keyword", $this->createKeyword($i));
            $relationObjects->add($relationObject);
        }
        return $relationObjects;
    }
    
    private function createKeyword($id) {
		$keyword = $this->keywordType->newInstance();
		$keyword->setId($id);
		$keyword->set("name", "keyword");
		return $keyword;
	}
}